// driver.cpp
// Written by Jaeseok Kim (jseokkim@hotmail.com)
// 03/29/2010

#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "../../amazon/src/date.h"

using namespace std;

class PurchasesByCustomer
{
public:
    // Constructor
    PurchasesByCustomer(int nInputCustomerId, int nInputNumOfPurchases, int nInputPurchaseSumInCents) :
        nCustomerId(nInputCustomerId), nNumOfPurchases(nInputNumOfPurchases), nPurchaseSumInCents(nInputPurchaseSumInCents)
    {
        int n, nDollar, nCent;
        vector<int> CumSum;

        // Split the total purchase sum into multiple purchases
        // Randomly generate cumulative purchase sum.
        // Currently, cumulative purchase sum is non-decreasing sequence, which may have purchase_amount as zero.
        // It can be changed to reflect real work purchase data. 
        // So, the encapsulation of the multiple-purchase-generating-algorithm will be a good design for future maintenance. 
        for (n = 0; n < nNumOfPurchases - 1; n++)
        {
            nDollar = (nPurchaseSumInCents < 100) ? 0 : (rand() % (nPurchaseSumInCents / 100));
            nCent = (rand() % 100);
            CumSum.push_back(nDollar * 100 + nCent);
        }
        sort(CumSum.begin(), CumSum.end());

        // Calculate each purchase from cumulative purchase sum
        // Put it into purchase array
        for (n = 0; n < nNumOfPurchases - 1; n++)
        {
            Purchases.push_back((n == 0) ? CumSum[n] : CumSum[n] - CumSum[n - 1]);
        }

        // If there is a single purchase, there is no CumSum. 
        // So, put the final PurchaseSum to purchase array
        Purchases.push_back((nNumOfPurchases == 1) ? nPurchaseSumInCents : (nPurchaseSumInCents - CumSum[n - 1]));
    }

    // Copy constructor
    PurchasesByCustomer(const PurchasesByCustomer &InputPurchases)
    {
        nCustomerId = InputPurchases.getCustomerId();
        nNumOfPurchases = InputPurchases.getNumOfPurchases();
        nPurchaseSumInCents = InputPurchases.getPurchaseSumInCents();
        Purchases = vector<int> (InputPurchases.Purchases);
    }

    int getCustomerId() const {return nCustomerId;}
    int getNumOfPurchases() const {return nNumOfPurchases;}
    int getPurchaseSumInCents() const {return nPurchaseSumInCents;}
    int getPurchase(int nIndex) const {return Purchases[nIndex];}
    int popPurchase()
    {
        int nPurchase = Purchases.back();
        Purchases.pop_back();
        nNumOfPurchases--;
        return nPurchase;
    }
    void print(){
        cout << nCustomerId << "\t" << nNumOfPurchases;
        for (int n = 0; n < nNumOfPurchases; n++)
        {
            cout << "\t" << (Purchases[n] / 100) << "." << (Purchases[n] % 100);
        }
        cout << endl;
    }

private:
    int nCustomerId;
    int nNumOfPurchases;
    int nPurchaseSumInCents;
    vector<int> Purchases;
};

class ExpectedResult
{
public:
    // Constructor
    ExpectedResult(int, int, int, int, int, int, int);
    void toFile();
    int getNumOfRecords() const {return nNumOfRecords;}
    bool empty() const {return PurchaseHistory.empty();}
    int size() const {return PurchaseHistory.size();}
    PurchasesByCustomer popRandomPurchaseRecord();

private:
    int nNumOfTopCustomers;
    int nNumOfDistinctCustomers;
    int nAvgNumOfPurchasesPerCustomer;
    int nMinQualifiedPurchaseSum;
    int nMinPurchaseSum;
    int nMaxPurchaseSum;
    int nNumOfQualifiedCustomers;
    int nNumOfRecords;
    vector<PurchasesByCustomer> PurchaseHistory;
};

ExpectedResult::ExpectedResult(int nInputNumOfTopCustomers, int nInputNumOfDistinctCustomers, int nInputAvgNumOfPurchasesPerCustomer, 
    int nInputMinQualifiedPurchaseSum, int nInputMinPurchaseSum, int nInputMaxPurchaseSum, int nInputNumOfQualifiedCustomers) :
    nNumOfTopCustomers(nInputNumOfTopCustomers), nNumOfDistinctCustomers(nInputNumOfDistinctCustomers), 
    nAvgNumOfPurchasesPerCustomer(nInputAvgNumOfPurchasesPerCustomer), nMinQualifiedPurchaseSum(nInputMinQualifiedPurchaseSum), 
    nMinPurchaseSum(nInputMinPurchaseSum), nMaxPurchaseSum(nInputMaxPurchaseSum), nNumOfQualifiedCustomers(nInputNumOfQualifiedCustomers)
{
    int n;

    // Create randomly shuffled distinct customer IDs
    vector<int> CustomerId;
    for (n = 0; n < nNumOfDistinctCustomers; n++)
    {
        CustomerId.push_back(n + 1);
    }
    random_shuffle(CustomerId.begin(), CustomerId.end());

    // Create qualified and unqualified purchase sum amounts within given range
    int nQualifiedPurchase, nUnqualifiedPurchase;
    vector<int> PurchaseSum;
    for (n = 0; n < nNumOfQualifiedCustomers; n++)
    {
        nQualifiedPurchase = (rand() % (nMaxPurchaseSum - nMinQualifiedPurchaseSum)) + nMinQualifiedPurchaseSum;
        PurchaseSum.push_back(nQualifiedPurchase);
    }
    for (; n < nNumOfDistinctCustomers; n++)
    {
        nUnqualifiedPurchase = (rand() % (nMinQualifiedPurchaseSum - nMinPurchaseSum)) + nMinPurchaseSum;
        PurchaseSum.push_back(nUnqualifiedPurchase);
    }

    // Sort purchase amounts by ascending order
    sort(PurchaseSum.begin(), PurchaseSum.end());

cout << "max == " << PurchaseSum[nNumOfDistinctCustomers - 1] << endl; 
cout << "min == " << PurchaseSum[nNumOfDistinctCustomers - nNumOfQualifiedCustomers] << endl; 
cout << "next_min == " << PurchaseSum[nNumOfDistinctCustomers - 1 - nNumOfQualifiedCustomers] << endl; 
cout << "Size of PurchaseSum == " << PurchaseSum.size() << endl; 

    // Create number of purchases for each customer
    // Note that a triangular distributed random number generator is used
    int nNumOfPurchases;
    vector<int> NumOfPurchases;
    for (n = 0; n < nNumOfDistinctCustomers; n++)
    {
        nNumOfPurchases = rand() % (nAvgNumOfPurchasesPerCustomer * 100);
        nNumOfPurchases += rand() % (nAvgNumOfPurchasesPerCustomer * 100);
        nNumOfPurchases = nNumOfPurchases / 100 + 1;
        NumOfPurchases.push_back(nNumOfPurchases);
    }

    // Create purchase history from three arrays (CustomerID, PurchaseSum, NumOfPurchases)
    for (n = 0, nNumOfRecords = 0; n < nNumOfDistinctCustomers; n++)
    {
        // Insert purchase history
        PurchaseHistory.push_back(PurchasesByCustomer(CustomerId.back(), NumOfPurchases.back(), PurchaseSum.back()));
//PurchaseHistory.back().print();
        nNumOfRecords += NumOfPurchases.back();
        // Release the already-inserted record
        CustomerId.pop_back(); 
        NumOfPurchases.pop_back(); 
        PurchaseSum.pop_back(); 
    }
cout << "Number of Purchase records == " << nNumOfRecords << endl;
}

void ExpectedResult::toFile()
{
    string sFile = "expected.result";
    ofstream ofResult(sFile.c_str());
    if (!ofResult)
    {
        cerr << "File open error[" << sFile << "]" << endl;
        return; 
    }

    int nNumOfCustomersInResult = min(nNumOfQualifiedCustomers, nNumOfTopCustomers);
    for (int t = 0; t < nNumOfCustomersInResult; t++)
    {
        ostringstream ossCents;
        ossCents.fill('0');
        ossCents.width(2);
        ossCents << (PurchaseHistory[t].getPurchaseSumInCents() % 100);

        ofResult << PurchaseHistory[t].getCustomerId() << " " <<  
            (PurchaseHistory[t].getPurchaseSumInCents() / 100) << "." << ossCents.str() << endl;
    }

    ofResult.close();
}

PurchasesByCustomer ExpectedResult::popRandomPurchaseRecord()
{
    size_t nRandom = rand() % PurchaseHistory.size();
//cout << "PurchaseHistory.size() == " << PurchaseHistory.size() << "\t" << "nRandom == " << nRandom << endl;     
    PurchasesByCustomer PurchaseRecord(PurchaseHistory[nRandom]);
//PurchaseRecord.print();
    
    // If the purchase is the last one in the record, delete from PurchaseHistory
    PurchaseHistory[nRandom].popPurchase();
    nNumOfRecords--;
    if (PurchaseHistory[nRandom].getNumOfPurchases() == 0)
    {
        PurchaseHistory.erase(PurchaseHistory.begin() + nRandom);
        nNumOfDistinctCustomers--;
    }

    return PurchaseRecord;
}

class LogFiles
{
public:
    LogFiles(string sInputEndDate, int nInputNumOfFiles, ExpectedResult *pInputResult) : 
        sEndDate(sInputEndDate), nNumOfFiles(nInputNumOfFiles), pResult(pInputResult) {}
    void toFiles();

private:
    string sEndDate;
    int nNumOfFiles;
    // ExpectedResult can occupy large memory. So, the pointer will be used to reduce copying overhead.
    ExpectedResult *pResult;
};

void LogFiles::toFiles()
{
    string sFileName;
    Date CurrDate(sEndDate.c_str());
    int nNumOfRecordsPerFile = pResult->getNumOfRecords() / nNumOfFiles;
    char szBuffer[8192];

    for (int nFile = 1, nRecord = 1; nFile <= nNumOfFiles; nFile++, CurrDate.prevDate())
    {
        // Open a log file
        sFileName = "purchase.log." + CurrDate.toString();
        cout << sFileName << endl;
        ofstream ofLog(sFileName.c_str());
        if (!ofLog)
        {
            cerr << "File open error[" << sFileName << "]" << endl;
            return;
        }
        ofLog.rdbuf()->pubsetbuf(szBuffer, sizeof(szBuffer));

        for (; nRecord <= nFile * nNumOfRecordsPerFile; nRecord++)
        {
            // Randomly pop a purchase record
            PurchasesByCustomer PurchaseRecord(pResult->popRandomPurchaseRecord());
            int nPurchaseInCents = PurchaseRecord.popPurchase();

            ostringstream ossCents;
            ossCents.fill('0');
            ossCents.width(2);
            ossCents << (nPurchaseInCents % 100);
            ofLog << nRecord << " " << PurchaseRecord.getCustomerId() << " " << nRecord << " " << 
                (nPurchaseInCents / 100) << "." << ossCents.str() << endl;
        }

        // For the last log file, flush all records
        if (nFile == nNumOfFiles)
        {
            for (; pResult->getNumOfRecords() > 0; nRecord++)
            {
                // Randomly pop a purchase record
                PurchasesByCustomer PurchaseRecord(pResult->popRandomPurchaseRecord());
                int nPurchaseInCents = PurchaseRecord.popPurchase();

                ostringstream ossCents;
                ossCents.fill('0');
                ossCents.width(2);
                ossCents << (nPurchaseInCents % 100);
                ofLog << nRecord << " " << PurchaseRecord.getCustomerId() << " " << nRecord << " " << 
                    (nPurchaseInCents / 100) << "." << ossCents.str() << endl;
            }
        }

        ofLog.close();
    }
}

typedef map<string, string> PropertyMap;

// base class for all test cases
class Test
{
public:
    Test(){}
    Test(PropertyMap InputProperties) : Properties(InputProperties){}
//    Test(const Test &InputTest) : Properties(InputTest.getProperties()){} 
    virtual ~Test(){}
    virtual void setup(){}
    virtual void testMethod(){}
    virtual void cleanup(){}
    PropertyMap getProperties() const {return Properties;}
    void printProperties();
    string getProperty(string);
    int getNumericProperty(string);
    bool VerifyOutput(string, string);

protected:
    PropertyMap Properties;
};

void Test::printProperties()
{
    for (PropertyMap::iterator i = Properties.begin(); i != Properties.end(); i++)
        cout << i->first << " = " << i->second << endl;
}

string Test::getProperty(string sPropertyName)
{
    PropertyMap::iterator itProperty = Properties.find(sPropertyName);
    return (itProperty == Properties.end()) ? NULL : itProperty->second;
}

int Test::getNumericProperty(string sPropertyName)
{
    int nValue = -1;

    PropertyMap::iterator itProperty = Properties.find(sPropertyName);
    if (itProperty != Properties.end())
    {
        istringstream(itProperty->second) >> nValue;
    }

    return nValue;
}

bool Test::VerifyOutput(string sOutputFile, string sExpectedOutput)
{
    string sLine;
    ifstream ifOutput(sOutputFile.c_str());
    if (ifOutput.is_open())
    {
        while (getline(ifOutput, sLine))
        {
            if (sLine == sExpectedOutput)
                return true;
        }
    }

    return false;
}

class VerifyInputDate : public Test
{
public:
    VerifyInputDate(PropertyMap InputProperties) : Test(InputProperties){}
//    VerifyInputDate(const VerifyInputDate &InputVerify) : Test(InputVerify.getProperties()){}
    void testMethod();
};

void VerifyInputDate::testMethod()
{
    string sExecutable = getProperty("TestExecutable");
    string sInputDate = getProperty("InputDate");
    string sOutputFile = getProperty("ConsoleOutputFile");
    string sExpectedOutput = getProperty("ExpectedConsoleOutput");
    string sDescription = getProperty("Description");

    cout << endl << "Running VerifyInputDate : " << sDescription << endl;
    string sCmd = sExecutable + " " + sInputDate + " > " + sOutputFile;
    cout << sCmd << endl; 
    system(sCmd.c_str());
    
    string sResult = VerifyOutput(sOutputFile, sExpectedOutput) ? "PASS" : "FAIL";
    cout << "TEST RESULT:" << sResult << endl;
}

class VerifyPurchaseStats : public Test
{
public:
    VerifyPurchaseStats(PropertyMap InputProperties) : Test(InputProperties){}
//    VerifyPurchaseStats(const VerifyPurchaseStats &InputVerify) : Test(InputVerify.getProperties()){}
    void setup();
    void testMethod();
    void cleanup();
    bool VerifyStatsOutput(string, string);
};

bool VerifyPurchaseStats::VerifyStatsOutput(string sOutputFile, string sExpectedResultFile)
{
    int nMismatchedCount = 0;
    int nCustomerId, nExpectedCustomerId;
    double dPurchaseAmount, dExpectedPurchaseAmount;
    
    ifstream ifOutput(sOutputFile.c_str());
    ifstream ifResult(sExpectedResultFile.c_str());
    
    if (ifOutput.is_open() && ifResult.is_open())
    {
        while (!ifOutput.eof() || !ifResult.eof())
        {
            ifOutput >> nCustomerId >> dPurchaseAmount;
            ifResult >> nExpectedCustomerId >> dExpectedPurchaseAmount;
            if (!ifOutput.eof() || !ifResult.eof())
            {
                if (nCustomerId != nExpectedCustomerId || dPurchaseAmount != dExpectedPurchaseAmount)
                {
                    cout << "Mismatched:CustomerId[" << nCustomerId << "," << nExpectedCustomerId << 
                        "], PurchaseAmount[" << dPurchaseAmount << "," << dExpectedPurchaseAmount << "]" << endl;
                    nMismatchedCount++;
                }
            }
        }
        
        ifResult.close();
        ifOutput.close();
    }
    else
    {
        cout << "File open error" << endl;
        return false;
    }
    
    return (nMismatchedCount == 0);
}

void VerifyPurchaseStats::setup()
{
    string sInputDate = getProperty("InputDate");
    int nNumOfTopCustomers = getNumericProperty("NumOfTopCustomers");
    int nNumOfDistinctCustomers = getNumericProperty("NumOfDistinctCustomers");
    int nAvgNumOfPurchasesPerCustomer = getNumericProperty("AvgNumOfPurchasesPerCustomer");
    int nMinQualifiedPurchaseSumInCents = getNumericProperty("MinQualifiedPurchaseSumInCents");
    int nMinPurchaseSumInCents = getNumericProperty("MinPurchaseSumInCents");
    int nMaxPurchaseSumInCents = getNumericProperty("MaxPurchaseSumInCents");
    int nNumOfQualifiedCustomers = getNumericProperty("NumOfQualifiedCustomers");

    cout << endl << "====Setting up VerifyPurchaseStats====" << endl;
    ExpectedResult Result(nNumOfTopCustomers, nNumOfDistinctCustomers, nAvgNumOfPurchasesPerCustomer, 
        nMinQualifiedPurchaseSumInCents, nMinPurchaseSumInCents, nMaxPurchaseSumInCents, nNumOfQualifiedCustomers);
    // Generate the expected output file
    Result.toFile();
    
    int nNumOfLogFiles = getNumericProperty("NumOfLogFiles");
    LogFiles Log(sInputDate, nNumOfLogFiles, &Result);
    // Generate log files from the expected result
    Log.toFiles();
}

void VerifyPurchaseStats::testMethod()
{
    string sExecutable = getProperty("TestExecutable");
    string sInputDate = getProperty("InputDate");
    string sOutputFile = getProperty("ConsoleOutputFile");
    string sExpectedOutputFile = getProperty("ExpectedResultFile");
    string sDescription = getProperty("Description");

    cout << "Running VerifyPurchaseStats : " << sDescription << endl;
    string sCmd = sExecutable + " " + sInputDate + " > " + sOutputFile;
    cout << sCmd << endl; 
    system(sCmd.c_str());
    
    string sResult = VerifyStatsOutput(sOutputFile, sExpectedOutputFile) ? "PASS" : "FAIL";
    cout << "TEST RESULT:" << sResult << endl;
}

void VerifyPurchaseStats::cleanup()
{
    cout << "--==Cleaning up VerifyPurchaseStats==--" << endl;

    // Delete expected result file
    string sExpectedOutputFile = getProperty("ExpectedResultFile");
    cout << "Remove " << sExpectedOutputFile << endl;
    remove(sExpectedOutputFile.c_str());

    // Delete all log files
    string sFileName;
    Date CurrDate(getProperty("InputDate").c_str());
    int nNumOfLogFiles = getNumericProperty("NumOfLogFiles");
    for (int n = 0; n < nNumOfLogFiles; n++, CurrDate.prevDate())
    {
        sFileName = string("purchase.log." + CurrDate.toString());
        cout << "Remove " << sFileName << endl;
        remove(sFileName.c_str());
    }  
}

class TestCollection : public Test
{
public:
    void testMethod();
    void loadTests(string);

private:
    vector<Test *> Tests;
};

void TestCollection::testMethod()
{
    for (vector<Test *>::iterator iTest = Tests.begin(); iTest < Tests.end(); iTest++)
    {
        (*iTest)->setup();
        (*iTest)->testMethod();
        (*iTest)->cleanup();
    }
}

void TestCollection::loadTests(string sFileName)
{
    // Open a configuration file
    ifstream ifConfig(sFileName.c_str());
    if (!ifConfig)
    {
        // In case of file open error, continue to the next file.
        cout << "File open error[" << sFileName << "]" << endl;
        return;
    }

    // Read property name and value
    string sPropertyName, sPropertyValue, sTestName;
    while (!ifConfig.eof())
    {
        // Look for "Begin"
        do
        {
            ifConfig >> sPropertyName >> ws;
        }
        while (!ifConfig.eof() && sPropertyName != "Begin");

        PropertyMap Properties;
        while (!ifConfig.eof())
        {
            ifConfig >> sPropertyName >> ws;
            if (sPropertyName == "End")
            {
                // Create a test case with built property map
                if (sTestName == "VerifyInputDate")
                {
                    Tests.push_back(new VerifyInputDate(Properties));
                }
                else if (sTestName == "VerifyPurchaseStats")
                {
                    Tests.push_back(new VerifyPurchaseStats(Properties));
                }
                // Move on to next test case
                break;
            }
            else
            {
                // Parse property name and value
                getline(ifConfig, sPropertyValue);
                if (sPropertyName == "TestName")
                {
                    sTestName = sPropertyValue;
                }
                else if (sPropertyName[0] != '#')
                {
                    // Build property map
                    Properties[sPropertyName] = sPropertyValue;
                }
            }
        }
    }
}

int main(int argc, char *argv[])
{
    // Check the number of input arguments
    if (argc != 2)
    {
        cout << "Usage: " << argv[0] << " test_case_config_file" << endl;
//        return 1;
    }

    TestCollection Tests;
    // Load test configuration file
    Tests.loadTests(string(argv[1]));
//    Tests.loadTests(string("test2.conf"));

    // Execute all test cases
    Tests.testMethod();

    return 0;
}
